﻿using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace FtpLibDotNet
{
    internal class FtpConnection
    {
        private const int BUFFER_SIZE = 512;

        /// <summary>
        /// Inidicates if connected to remote server
        /// </summary>
        public bool Connected { get; private set; }

        private Socket clientSocket;
        private string host;
        private int port;

        /// <summary>
        /// Occure when opens connection
        /// </summary>
        public event EventHandler ConnectionOpened;
        /// <summary>
        /// Occure when closes connection
        /// </summary>
        public event EventHandler ConnectionClosed;
        /// <summary>
        /// Occure when connection is closed by server
        /// </summary>
        public event EventHandler Disconnected;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="host">Remote server address</param>
        /// <param name="port">Remote server port (default 21)</param>
        internal FtpConnection(string host, int port = 21)
        {
            this.host = host;
            this.port = port;
            this.ConnectionOpened = this.OnConnectionOpened;
            this.ConnectionClosed = this.OnConnectionClosed;
            this.Disconnected = this.OnDisconnected;
            this.Connected = false;
        }

        protected void OnConnectionOpened(object sender, EventArgs e)
        {
            this.Connected = true;
        }

        protected void OnConnectionClosed(object sender, EventArgs e)
        {
            this.Connected = false;
        }

        protected void OnDisconnected(object sender, EventArgs e)
        {
            this.clientSocket = null;
            this.ConnectionClosed(sender, e);
        }

        /// <summary>
        /// Opens connection to remote server
        /// </summary>
        /// <exception cref="FtpConnectionException">Occures when could not connect to remote server.</exception>
        public void Open()
        {
            try
            {
                // Create socket
                this.clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                // Resolve address
                IPEndPoint ipe = null;
                try
                {
                    ipe = new IPEndPoint(Dns.GetHostAddresses(this.host)[0], this.port);
                }
                catch (Exception ex)
                {
                    this.Disconnected(this, new EventArgs());
                    throw new FtpConnectionException("Could not resolve address.", ex);
                }
                // Connect socket to remote address
                try
                {
                    this.clientSocket.Connect(ipe);
                    FtpResponse welcomeResponse = this.ReadResponse();
                    if (welcomeResponse.Code != 220)
                    {
                        throw new FtpConnectionException(welcomeResponse.Response);
                    }
                }
                catch (Exception ex)
                {
                    this.Disconnected(this, new EventArgs());
                    throw new FtpConnectionException("Could not connect to remote server.", ex);
                }

                this.ConnectionOpened(this, new EventArgs());
            }
            catch (Exception ex)
            {
                this.Disconnected(this, new EventArgs());
                throw new FtpConnectionException("Unknown exception occured while connecting to ftp server.", ex);
            }
        }

        /// <summary>
        /// Closes opened connection to remote server
        /// </summary>
        /// <exception cref="FtpConnectionException">Occures when not connected.</exception>
        public void Close()
        {
            if (!this.Connected)
                throw new FtpConnectionException("Client not connected.");

            // Send quit message
            this.SendCommand(FtpCommands.QUIT());
            // If still connected then disconnect
            if (this.clientSocket != null && this.clientSocket.Connected)
            {
                this.clientSocket.Close();
                this.clientSocket = null;
            }
            this.ConnectionClosed(this, new EventArgs());
        }

        /// <summary>
        /// Sends command and wait for response
        /// </summary>
        /// <param name="command">Command to remote server</param>
        /// <returns>Response from remote server</returns>
        /// <exception cref="FtpException">Occures when remote server disconnected or unknown error occured.</exception>
        public FtpResponse SendCommand(string command)
        {
            try
            {
                this.SendData(command);
                return this.ReadResponse();
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode == 10035        // 10035 Resource temporarily unavailable
                    || ex.ErrorCode == 10050     // 10050 Network is down
                    || ex.ErrorCode == 10051     // 10051 Network is unreachable
                    || ex.ErrorCode == 10052     // 10052 Network dropped connection on reset
                    || ex.ErrorCode == 10053     // 10053 Software caused connection abort
                    || ex.ErrorCode == 10060     // 10060 Connection timed out
                    || ex.ErrorCode == 10064)    // 10064 Host is down
                {
                    this.clientSocket = null;
                    this.Disconnected(this, new EventArgs());
                    throw new FtpException("Could not send command. Client disconnected.", ex);
                }
                else
                    throw new FtpException("Could not send command.", ex);
            }
        }

        /// <summary>
        /// Sends data to remote server
        /// </summary>
        /// <param name="data">String data</param>
        /// <exception cref="FtpConnectionException">Occures when client not connected to remote server.</exception>
        protected void SendData(string data)
        {
            this.SendData(Encoding.ASCII.GetBytes((data + "\r\n").ToCharArray()));
        }

        /// <summary>
        /// Sends data to remote server
        /// </summary>
        /// <param name="data">Binary data</param>
        /// <exception cref="FtpConnectionException">Occures when client not connected to remote server.</exception>
        protected void SendData(byte[] data)
        {
            if (!this.Connected || this.clientSocket == null || !this.clientSocket.Connected)
                throw new FtpConnectionException("Client not connected.");

            this.clientSocket.Send(data);
        }

        /// <summary>
        /// Reads response from socket stream
        /// </summary>
        /// <returns>Response from remote server</returns>
        internal FtpResponse ReadResponse()
        {
            return this.ReadData();
        }

        /// <summary>
        /// Reads data from socket stream
        /// </summary>
        /// <returns></returns>
        /// <exception cref="FtpConnectionException">Occures when client not connected to remote server.</exception>
        /// <exception cref="FtpException">Occures when could not read from stream.</exception>
        protected string ReadData(int level = 0)
        {
            if (this.clientSocket == null || !this.clientSocket.Connected)
                throw new FtpConnectionException("Client not connected.");

            int receivedBytes;
            StringBuilder receivedData = new StringBuilder();
            byte[] buffer = new byte[BUFFER_SIZE];

            while (true)
            {
                receivedBytes = this.clientSocket.Receive(buffer, buffer.Length, 0);
                receivedData.Append(Encoding.ASCII.GetString(buffer, 0, receivedBytes));
                if (receivedBytes <= buffer.Length)
                {
                    break;
                }
            }

            char[] separators = { '\n' };
            string[] resultLines = receivedData.ToString().Split(separators);

            string result = String.Empty;

            if (resultLines.Length > 2)
                result = resultLines[resultLines.Length - 2];
            else
                result = resultLines[0];

            if (result.Length < 3 || result.Substring(3, 1) != " ")
            {
                if (level > 25)
                    throw new FtpException("Could not read data.");
                else
                    return this.ReadData(level + 1);
            }

            return result;
        }

        /// <summary>
        /// Sets connection mode to binary
        /// </summary>
        public void SetBinaryMode()
        {
            try
            {
                FtpResponse response = this.SendCommand(FtpCommands.TYPE("I"));
            }
            catch (FtpCommandException ex)
            {
                throw new FtpCommandException(ex.Response, "Could not set binary mode.", ex);
            }
            catch (Exception ex)
            {
                throw new FtpException("Could not set binary mode.", ex);
            }
        }

        /// <summary>
        /// Sets connection mode to text
        /// </summary>
        public void SetTextMode()
        {
            try
            {
                FtpResponse response = this.SendCommand(FtpCommands.TYPE("A"));
            }
            catch (FtpCommandException ex)
            {
                throw new FtpCommandException(ex.Response, "Could not set ascii mode.", ex);
            }
            catch (Exception ex)
            {
                throw new FtpException("Could not set ascii mode.", ex);
            }
        }

        /// <summary>
        /// Creates socket for data transfer
        /// </summary>
        /// <returns>Passive socket for data transfer</returns>
        /// <exception cref="FtpConnectionException">Occures when client not connected to remote server or could not connect to passive socket.</exception>
        /// <exception cref="FtpException">Occures when could not read data from reply.</exception>
        /// <exception cref="FtpCommandException">Occures when remote server give wrong response.</exception>
        public Socket CreateDataSocket()
        {
            FtpResponse pasvResponse = this.SendCommand(FtpCommands.PASV());
            if (pasvResponse.Code != 227)
                throw new FtpCommandException(pasvResponse, "PASV command failed.");

            int beginIndex = pasvResponse.Data.IndexOf('(');
            int endIndex = pasvResponse.Data.IndexOf(')');
            string ipString = pasvResponse.Data.Substring(beginIndex + 1, endIndex - beginIndex - 1);
            int[] ipData = new int[6];
            string[] ipStringData = ipString.Split(',');

            if (ipStringData.Length < ipData.Length)
                throw new FtpException(String.Format("Malformed PASV reply: {0}", pasvResponse.Data));

            for (int i = 0; i < ipData.Length && i < ipStringData.Length; i++)
            {
                try
                {
                    ipData[i] = Int32.Parse(ipStringData[i]);
                }
                catch (Exception ex)
                {
                    throw new FtpException(String.Format("Malformed PASV reply: {0}", pasvResponse.Data), ex);
                }
            }

            int socketPort = (ipData[4] << 8) + ipData[5];
            string socketAddress = String.Format("{0}.{1}.{2}.{3}", ipData[0], ipData[1], ipData[2], ipData[3]);

            Socket result = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint resultEndPoint = null;
            try
            {
                resultEndPoint = new IPEndPoint(Dns.GetHostAddresses(socketAddress)[0], socketPort);
            }
            catch(Exception ex)
            {
                throw new FtpConnectionException("Could not resolve remote address.", ex);
            }

            try
            {
                result.Connect(resultEndPoint);
            }
            catch(Exception ex)
            {
                throw new FtpConnectionException("Could not connect to remote server.", ex);
            }
            return result;
        }
    }
}